/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.catalina.manager;

import org.apache.catalina.*;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.core.StandardServer;
import org.apache.catalina.startup.ExpandWar;
import org.apache.catalina.util.ContextName;
import org.apache.catalina.util.RequestUtil;
import org.apache.catalina.util.ServerInfo;
import org.apache.tomcat.util.Diagnostics;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.modeler.Registry;
import org.apache.tomcat.util.res.StringManager;

import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.naming.Binding;
import javax.naming.NamingEnumeration;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.UnavailableException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.Enumeration;
import java.util.Locale;

/**
 * Servlet that enables remote management of the web applications installed
 * within the same virtual host as this web application is.  Normally, this
 * functionality will be protected by a security constraint in the web
 * application deployment descriptor.  However, this requirement can be
 * relaxed during testing.
 * <p>
 * This servlet examines the value returned by <code>getPathInfo()</code>
 * and related query parameters to determine what action is being requested.
 * The following actions and parameters (starting after the servlet path)
 * are supported:
 * <ul>
 * <li><b>/deploy?config={config-url}</b> - Install and start a new
 * web application, based on the contents of the context configuration
 * file found at the specified URL.  The <code>docBase</code> attribute
 * of the context configuration file is used to locate the actual
 * WAR or directory containing the application.</li>
 * <li><b>/deploy?config={config-url}&war={war-url}/</b> - Install and start
 * a new web application, based on the contents of the context
 * configuration file found at <code>{config-url}</code>, overriding the
 * <code>docBase</code> attribute with the contents of the web
 * application archive found at <code>{war-url}</code>.</li>
 * <li><b>/deploy?path=/xxx&war={war-url}</b> - Install and start a new
 * web application attached to context path <code>/xxx</code>, based
 * on the contents of the web application archive found at the
 * specified URL.</li>
 * <li><b>/list</b> - List the context paths of all currently installed web
 * applications for this virtual host.  Each context will be listed with
 * the following format <code>path:status:sessions</code>.
 * Where path is the context path.  Status is either running or stopped.
 * Sessions is the number of active Sessions.</li>
 * <li><b>/reload?path=/xxx</b> - Reload the Java classes and resources for
 * the application at the specified path.</li>
 * <li><b>/resources?type=xxxx</b> - Enumerate the available global JNDI
 * resources, optionally limited to those of the specified type
 * (fully qualified Java class name), if available.</li>
 * <li><b>/serverinfo</b> - Display system OS and JVM properties.
 * <li><b>/sessions</b> - Deprecated. Use expire.
 * <li><b>/expire?path=/xxx</b> - List session idle timeinformation about the
 * web application attached to context path <code>/xxx</code> for this
 * virtual host.</li>
 * <li><b>/expire?path=/xxx&idle=mm</b> - Expire sessions
 * for the context path <code>/xxx</code> which were idle for at
 * least mm minutes.</li>
 * <li><b>/start?path=/xxx</b> - Start the web application attached to
 * context path <code>/xxx</code> for this virtual host.</li>
 * <li><b>/stop?path=/xxx</b> - Stop the web application attached to
 * context path <code>/xxx</code> for this virtual host.</li>
 * <li><b>/threaddump</b> - Write a JVM thread dump.</li>
 * <li><b>/undeploy?path=/xxx</b> - Shutdown and remove the web application
 * attached to context path <code>/xxx</code> for this virtual host,
 * and remove the underlying WAR file or document base directory.
 * (<em>NOTE</em> - This is only allowed if the WAR file or document
 * base is stored in the <code>appBase</code> directory of this host,
 * typically as a result of being placed there via the <code>/deploy</code>
 * command.</li>
 * <li><b>/vminfo</b> - Write some VM info.</li>
 * </ul>
 * <p>Use <code>path=/</code> for the ROOT context.</p>
 * <p>The syntax of the URL for a web application archive must conform to one
 * of the following patterns to be successfully deployed:</p>
 * <ul>
 * <li><b>file:/absolute/path/to/a/directory</b> - You can specify the absolute
 * path of a directory that contains the unpacked version of a web
 * application.  This directory will be attached to the context path you
 * specify without any changes.</li>
 * </ul>
 * <p>
 * <b>NOTE</b> - Attempting to reload or remove the application containing
 * this servlet itself will not succeed.  Therefore, this servlet should
 * generally be deployed as a separate web application within the virtual host
 * to be managed.
 * <p>
 * The following servlet initialization parameters are recognized:
 * <ul>
 * <li><b>debug</b> - The debugging detail level that controls the amount
 * of information that is logged by this servlet.  Default is zero.
 * </ul>
 *
 * @author Craig R. McClanahan
 * @author Remy Maucherat
 */
public class ManagerServlet extends HttpServlet implements ContainerServlet {

	protected static final boolean LAST_ACCESS_AT_START;
	/**
	 * The string manager for this package.
	 */
	protected static final StringManager sm =
			StringManager.getManager(Constants.Package);
	private static final long serialVersionUID = 1L;

	// ----------------------------------------------------- Instance Variables

	static {
		String lastAccessAtStart = System.getProperty(
				"org.apache.catalina.session.StandardSession.LAST_ACCESS_AT_START");
		if (lastAccessAtStart == null) {
			LAST_ACCESS_AT_START = Globals.STRICT_SERVLET_COMPLIANCE;
		} else {
			LAST_ACCESS_AT_START = Boolean.parseBoolean(lastAccessAtStart);
		}
	}

	/**
	 * Path where context descriptors should be deployed.
	 */
	protected File configBase = null;
	/**
	 * The Context container associated with our web application.
	 */
	protected transient Context context = null;
	/**
	 * The debugging detail level for this servlet.
	 */
	protected int debug = 1;
	/**
	 * File object representing the directory into which the deploy() command
	 * will deploy uploaded WAR files (normally the appBase).
	 */
	protected File deployed = null;
	/**
	 * Path used to store revisions of webapps.
	 */
	protected File versioned = null;
	/**
	 * Path used to store context descriptors.
	 *
	 * @deprecated Unused
	 */
	@Deprecated
	protected File contextDescriptors = null;
	/**
	 * The associated host.
	 */
	protected transient Host host = null;
	/**
	 * The host appBase.
	 *
	 * @deprecated Unused
	 */
	@Deprecated
	protected File appBase = null;
	/**
	 * MBean server.
	 */
	protected transient MBeanServer mBeanServer = null;
	/**
	 * The associated deployer ObjectName.
	 */
	protected ObjectName oname = null;
	/**
	 * The global JNDI <code>NamingContext</code> for this server,
	 * if available.
	 */
	protected transient javax.naming.Context global = null;
	/**
	 * The Wrapper container associated with this servlet.
	 */
	protected transient Wrapper wrapper = null;


	// ----------------------------------------------- ContainerServlet Methods

	protected static boolean validateContextName(ContextName cn,
	                                             PrintWriter writer, StringManager sm) {

		// ContextName should be non-null with a path that is empty or starts
		// with /
		if (cn != null &&
				(cn.getPath().startsWith("/") || cn.getPath().equals(""))) {
			return true;
		}

		String path = null;
		if (cn != null) {
			path = RequestUtil.filter(cn.getPath());
		}
		writer.println(sm.getString("managerServlet.invalidPath", path));
		return false;
	}

	/**
	 * Copy the specified file or directory to the destination.
	 *
	 * @param src  File object representing the source
	 * @param dest File object representing the destination
	 */
	public static boolean copy(File src, File dest) {
		boolean result = false;
		try {
			if (src != null &&
					!src.getCanonicalPath().equals(dest.getCanonicalPath())) {
				result = copyInternal(src, dest, new byte[4096]);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		return result;
	}


	// --------------------------------------------------------- Public Methods

	/**
	 * Copy the specified file or directory to the destination.
	 *
	 * @param src  File object representing the source
	 * @param dest File object representing the destination
	 */
	public static boolean copyInternal(File src, File dest, byte[] buf) {

		boolean result = true;

		String files[] = null;
		if (src.isDirectory()) {
			files = src.list();
			result = dest.mkdir();
		} else {
			files = new String[1];
			files[0] = "";
		}
		if (files == null) {
			files = new String[0];
		}
		for (int i = 0; (i < files.length) && result; i++) {
			File fileSrc = new File(src, files[i]);
			File fileDest = new File(dest, files[i]);
			if (fileSrc.isDirectory()) {
				result = copyInternal(fileSrc, fileDest, buf);
			} else {
				FileInputStream is = null;
				FileOutputStream os = null;
				try {
					is = new FileInputStream(fileSrc);
					os = new FileOutputStream(fileDest);
					int len = 0;
					while (true) {
						len = is.read(buf);
						if (len == -1)
							break;
						os.write(buf, 0, len);
					}
				} catch (IOException e) {
					e.printStackTrace();
					result = false;
				} finally {
					if (is != null) {
						try {
							is.close();
						} catch (IOException e) {
							// Ignore
						}
					}
					if (os != null) {
						try {
							os.close();
						} catch (IOException e) {
							// Ignore
						}
					}
				}
			}
		}
		return result;

	}

	/**
	 * Return the Wrapper with which we are associated.
	 */
	@Override
	public Wrapper getWrapper() {

		return (this.wrapper);

	}

	/**
	 * Set the Wrapper with which we are associated.
	 *
	 * @param wrapper The new wrapper
	 */
	@Override
	public void setWrapper(Wrapper wrapper) {

		this.wrapper = wrapper;
		if (wrapper == null) {
			context = null;
			host = null;
			oname = null;
		} else {
			context = (Context) wrapper.getParent();
			host = (Host) context.getParent();
			Engine engine = (Engine) host.getParent();
			String name = engine.getName() + ":type=Deployer,host=" +
					host.getName();
			try {
				oname = new ObjectName(name);
			} catch (Exception e) {
				log(sm.getString("managerServlet.objectNameFail", name), e);
			}
		}

		// Retrieve the MBean server
		mBeanServer = Registry.getRegistry(null, null).getMBeanServer();

	}

	/**
	 * Finalize this servlet.
	 */
	@Override
	public void destroy() {

		// No actions necessary

	}


	// -------------------------------------------------------- Private Methods

	/**
	 * Process a GET request for the specified resource.
	 *
	 * @param request  The servlet request we are processing
	 * @param response The servlet response we are creating
	 * @throws IOException      if an input/output error occurs
	 * @throws ServletException if a servlet-specified error occurs
	 */
	@Override
	public void doGet(HttpServletRequest request,
	                  HttpServletResponse response)
			throws IOException, ServletException {

		StringManager smClient = StringManager.getManager(
				Constants.Package, request.getLocales());

		// Identify the request parameters that we need
		String command = request.getPathInfo();
		if (command == null)
			command = request.getServletPath();
		String config = request.getParameter("config");
		String path = request.getParameter("path");
		ContextName cn = null;
		if (path != null) {
			cn = new ContextName(path, request.getParameter("version"));
		}
		String type = request.getParameter("type");
		String war = request.getParameter("war");
		String tag = request.getParameter("tag");
		boolean update = false;
		if ((request.getParameter("update") != null)
				&& (request.getParameter("update").equals("true"))) {
			update = true;
		}

		boolean statusLine = false;
		if ("true".equals(request.getParameter("statusLine"))) {
			statusLine = true;
		}

		// Prepare our output writer to generate the response message
		response.setContentType("text/plain; charset=" + Constants.CHARSET);
		PrintWriter writer = response.getWriter();

		// Process the requested command
		if (command == null) {
			writer.println(smClient.getString("managerServlet.noCommand"));
		} else if (command.equals("/deploy")) {
			if (war != null || config != null) {
				deploy(writer, config, cn, war, update, smClient);
			} else if (tag != null) {
				deploy(writer, cn, tag, smClient);
			} else {
				writer.println(smClient.getString(
						"managerServlet.invalidCommand", command));
			}
		} else if (command.equals("/list")) {
			list(writer, smClient);
		} else if (command.equals("/reload")) {
			reload(writer, cn, smClient);
		} else if (command.equals("/resources")) {
			resources(writer, type, smClient);
		} else if (command.equals("/save")) {
			save(writer, path, smClient);
		} else if (command.equals("/serverinfo")) {
			serverinfo(writer, smClient);
		} else if (command.equals("/sessions")) {
			expireSessions(writer, cn, request, smClient);
		} else if (command.equals("/expire")) {
			expireSessions(writer, cn, request, smClient);
		} else if (command.equals("/start")) {
			start(writer, cn, smClient);
		} else if (command.equals("/stop")) {
			stop(writer, cn, smClient);
		} else if (command.equals("/undeploy")) {
			undeploy(writer, cn, smClient);
		} else if (command.equals("/findleaks")) {
			findleaks(statusLine, writer, smClient);
		} else if (command.equals("/vminfo")) {
			vmInfo(writer, smClient, request.getLocales());
		} else if (command.equals("/threaddump")) {
			threadDump(writer, smClient, request.getLocales());
		} else {
			writer.println(smClient.getString("managerServlet.unknownCommand",
					command));
		}

		// Finish up the response
		writer.flush();
		writer.close();

	}

	/**
	 * Process a PUT request for the specified resource.
	 *
	 * @param request  The servlet request we are processing
	 * @param response The servlet response we are creating
	 * @throws IOException      if an input/output error occurs
	 * @throws ServletException if a servlet-specified error occurs
	 */
	@Override
	public void doPut(HttpServletRequest request,
	                  HttpServletResponse response)
			throws IOException, ServletException {

		StringManager smClient = StringManager.getManager(
				Constants.Package, request.getLocales());

		// Identify the request parameters that we need
		String command = request.getPathInfo();
		if (command == null)
			command = request.getServletPath();
		String path = request.getParameter("path");
		ContextName cn = null;
		if (path != null) {
			cn = new ContextName(path, request.getParameter("version"));
		}
		String tag = request.getParameter("tag");
		boolean update = false;
		if ((request.getParameter("update") != null)
				&& (request.getParameter("update").equals("true"))) {
			update = true;
		}

		// Prepare our output writer to generate the response message
		response.setContentType("text/plain;charset=" + Constants.CHARSET);
		PrintWriter writer = response.getWriter();

		// Process the requested command
		if (command == null) {
			writer.println(smClient.getString("managerServlet.noCommand"));
		} else if (command.equals("/deploy")) {
			deploy(writer, cn, tag, update, request, smClient);
		} else {
			writer.println(smClient.getString("managerServlet.unknownCommand",
					command));
		}

		// Finish up the response
		writer.flush();
		writer.close();

	}

	/**
	 * Initialize this servlet.
	 */
	@Override
	public void init() throws ServletException {

		// Ensure that our ContainerServlet properties have been set
		if ((wrapper == null) || (context == null))
			throw new UnavailableException(
					sm.getString("managerServlet.noWrapper"));

		// Set our properties from the initialization parameters
		String value = null;
		try {
			value = getServletConfig().getInitParameter("debug");
			debug = Integer.parseInt(value);
		} catch (Throwable t) {
			ExceptionUtils.handleThrowable(t);
		}

		// Acquire global JNDI resources if available
		Server server = ((Engine) host.getParent()).getService().getServer();
		if (server != null) {
			global = server.getGlobalNamingContext();
		}

		// Calculate the directory into which we will be deploying applications
		versioned = (File) getServletContext().getAttribute
				(ServletContext.TEMPDIR);

		// Identify the appBase of the owning Host of this Context
		// (if any)
		String appBase = ((Host) context.getParent()).getAppBase();
		deployed = new File(appBase);
		if (!deployed.isAbsolute()) {
			deployed = new File(System.getProperty(Globals.CATALINA_BASE_PROP),
					appBase);
		}
		configBase = new File(System.getProperty(Globals.CATALINA_BASE_PROP), "conf");
		Container container = context;
		Container host = null;
		Container engine = null;
		while (container != null) {
			if (container instanceof Host)
				host = container;
			if (container instanceof Engine)
				engine = container;
			container = container.getParent();
		}
		if (engine != null) {
			configBase = new File(configBase, engine.getName());
		}
		if (host != null) {
			configBase = new File(configBase, host.getName());
		}
		// Note: The directory must exist for this to work.

		// Log debugging messages as necessary
		if (debug >= 1) {
			log("init: Associated with Deployer '" +
					oname + "'");
			if (global != null) {
				log("init: Global resources are available");
			}
		}

	}

	/**
	 * Find potential memory leaks caused by web application reload.
	 */
	protected void findleaks(boolean statusLine, PrintWriter writer,
	                         StringManager smClient) {

		if (!(host instanceof StandardHost)) {
			writer.println(smClient.getString("managerServlet.findleaksFail"));
			return;
		}

		String[] results =
				((StandardHost) host).findReloadedContextMemoryLeaks();

		if (results.length > 0) {
			if (statusLine) {
				writer.println(
						smClient.getString("managerServlet.findleaksList"));
			}
			for (String result : results) {
				if ("".equals(result)) {
					result = "/";
				}
				writer.println(result);
			}
		} else if (statusLine) {
			writer.println(smClient.getString("managerServlet.findleaksNone"));
		}
	}

	/**
	 * Write some VM info
	 *
	 * @param writer
	 */
	protected void vmInfo(PrintWriter writer, StringManager smClient,
	                      Enumeration<Locale> requestedLocales) {
		writer.println(smClient.getString("managerServlet.vminfo"));
		writer.print(Diagnostics.getVMInfo(requestedLocales));
	}

	/**
	 * Write a JVM thread dump
	 *
	 * @param writer
	 */
	protected void threadDump(PrintWriter writer, StringManager smClient,
	                          Enumeration<Locale> requestedLocales) {
		writer.println(smClient.getString("managerServlet.threaddump"));
		writer.print(Diagnostics.getThreadDump(requestedLocales));
	}

	/**
	 * Store server configuration.
	 *
	 * @param path Optional context path to save
	 */
	protected synchronized void save(PrintWriter writer, String path,
	                                 StringManager smClient) {

		Server server = ((Engine) host.getParent()).getService().getServer();

		if (!(server instanceof StandardServer)) {
			writer.println(smClient.getString("managerServlet.saveFail",
					server));
			return;
		}

		if ((path == null) || path.length() == 0 || !path.startsWith("/")) {
			try {
				((StandardServer) server).storeConfig();
				writer.println(smClient.getString("managerServlet.saved"));
			} catch (Exception e) {
				log("managerServlet.storeConfig", e);
				writer.println(smClient.getString("managerServlet.exception",
						e.toString()));
				return;
			}
		} else {
			String contextPath = path;
			if (path.equals("/")) {
				contextPath = "";
			}
			Context context = (Context) host.findChild(contextPath);
			if (context == null) {
				writer.println(smClient.getString("managerServlet.noContext",
						path));
				return;
			}
			try {
				((StandardServer) server).storeContext(context);
				writer.println(smClient.getString("managerServlet.savedContext",
						path));
			} catch (Exception e) {
				log("managerServlet.save[" + path + "]", e);
				writer.println(smClient.getString("managerServlet.exception",
						e.toString()));
				return;
			}
		}

	}

	/**
	 * Deploy a web application archive (included in the current request)
	 * at the specified context path.
	 *
	 * @param writer   Writer to render results to
	 * @param cn       Name of the application to be installed
	 * @param tag      Tag to be associated with the webapp
	 * @param update   Flag that indicates that any existing app should be
	 *                 replaced
	 * @param request  Servlet request we are processing
	 * @param smClient i18n messages using the locale of the client
	 */
	protected synchronized void deploy
	(PrintWriter writer, ContextName cn,
	 String tag, boolean update, HttpServletRequest request,
	 StringManager smClient) {

		if (debug >= 1) {
			log("deploy: Deploying web application '" + cn + "'");
		}

		// Validate the requested context path
		if (!validateContextName(cn, writer, smClient)) {
			return;
		}
		String name = cn.getName();
		String baseName = cn.getBaseName();
		String displayPath = cn.getDisplayName();

		// If app exists deployment can only proceed if update is true
		// Note existing WAR will be deleted and then replaced
		Context context = (Context) host.findChild(name);
		if (context != null && !update) {
			writer.println(smClient.getString("managerServlet.alreadyContext",
					displayPath));
			return;
		}

		File deployedWar = new File(deployed, baseName + ".war");

		// Determine full path for uploaded WAR
		File uploadedWar;
		if (tag == null) {
			if (update) {
				// Append ".tmp" to the file name so it won't get deployed if auto
				// deployment is enabled. It also means the old war won't get
				// deleted if the upload fails
				uploadedWar = new File(deployedWar.getAbsolutePath() + ".tmp");
				if (uploadedWar.exists() && !uploadedWar.delete()) {
					writer.println(smClient.getString("managerServlet.deleteFail",
							uploadedWar));
				}
			} else {
				uploadedWar = deployedWar;
			}
		} else {
			File uploadPath = new File(versioned, tag);
			if (!uploadPath.mkdirs() && !uploadPath.isDirectory()) {
				writer.println(smClient.getString("managerServlet.mkdirFail",
						uploadPath));
				return;
			}
			uploadedWar = new File(uploadPath, baseName + ".war");
		}
		if (debug >= 2) {
			log("Uploading WAR file to " + uploadedWar);
		}

		try {
			if (isServiced(name)) {
				writer.println(smClient.getString("managerServlet.inService", displayPath));
			} else {
				addServiced(name);
				try {
					// Upload WAR
					uploadWar(writer, request, uploadedWar, smClient);
					if (update && tag == null) {
						if (deployedWar.exists() && !deployedWar.delete()) {
							writer.println(smClient.getString("managerServlet.deleteFail",
									deployedWar));
							return;
						}
						// Rename uploaded WAR file
						uploadedWar.renameTo(deployedWar);
					}
					if (tag != null) {
						// Copy WAR to the host's appBase
						copy(uploadedWar, deployedWar);
					}
					// Perform new deployment
					check(name);
				} finally {
					removeServiced(name);
				}
			}
		} catch (Exception e) {
			log("managerServlet.check[" + displayPath + "]", e);
			writer.println(smClient.getString("managerServlet.exception",
					e.toString()));
			return;
		}

		writeDeployResult(writer, smClient, name, displayPath);
	}

	/**
	 * Install an application for the specified path from the specified
	 * web application archive.
	 *
	 * @param writer   Writer to render results to
	 * @param tag      Revision tag to deploy from
	 * @param cn       Name of the application to be installed
	 * @param smClient i18n messages using the locale of the client
	 */
	protected void deploy(PrintWriter writer, ContextName cn, String tag,
	                      StringManager smClient) {

		// NOTE: It is assumed that update is always true in this method.

		// Validate the requested context path
		if (!validateContextName(cn, writer, smClient)) {
			return;
		}

		String baseName = cn.getBaseName();
		String name = cn.getName();
		String displayPath = cn.getDisplayName();

		// Find the local WAR file
		File localWar = new File(new File(versioned, tag), baseName + ".war");

		File deployedWar = new File(deployed, baseName + ".war");

		// Copy WAR to appBase
		try {
			if (isServiced(name)) {
				writer.println(smClient.getString("managerServlet.inService", displayPath));
			} else {
				addServiced(name);
				try {
					if (!deployedWar.delete()) {
						writer.println(smClient.getString("managerServlet.deleteFail",
								deployedWar));
						return;
					}
					copy(localWar, deployedWar);
					// Perform new deployment
					check(name);
				} finally {
					removeServiced(name);
				}
			}
		} catch (Exception e) {
			log("managerServlet.check[" + displayPath + "]", e);
			writer.println(smClient.getString("managerServlet.exception",
					e.toString()));
			return;
		}

		writeDeployResult(writer, smClient, name, displayPath);
	}

	/**
	 * Install an application for the specified path from the specified
	 * web application archive.
	 *
	 * @param writer   Writer to render results to
	 * @param config   URL of the context configuration file to be installed
	 * @param cn       Name of the application to be installed
	 * @param war      URL of the web application archive to be installed
	 * @param update   true to override any existing webapp on the path
	 * @param smClient i18n messages using the locale of the client
	 */
	protected void deploy(PrintWriter writer, String config, ContextName cn,
	                      String war, boolean update, StringManager smClient) {

		if (config != null && config.length() == 0) {
			config = null;
		}
		if (war != null && war.length() == 0) {
			war = null;
		}

		if (debug >= 1) {
			if (config != null && config.length() > 0) {
				if (war != null) {
					log("install: Installing context configuration at '" +
							config + "' from '" + war + "'");
				} else {
					log("install: Installing context configuration at '" +
							config + "'");
				}
			} else {
				if (cn != null) {
					log("install: Installing web application '" + cn +
							"' from '" + war + "'");
				} else {
					log("install: Installing web application from '" + war + "'");
				}
			}
		}

		if (!validateContextName(cn, writer, smClient)) {
			return;
		}
		@SuppressWarnings("null") // checked in call above
				String name = cn.getName();
		String baseName = cn.getBaseName();
		String displayPath = cn.getDisplayName();

		// If app exists deployment can only proceed if update is true
		// Note existing files will be deleted and then replaced
		Context context = (Context) host.findChild(name);
		if (context != null && !update) {
			writer.println(smClient.getString("managerServlet.alreadyContext",
					displayPath));
			return;
		}

		if (config != null && (config.startsWith("file:"))) {
			config = config.substring("file:".length());
		}
		if (war != null && (war.startsWith("file:"))) {
			war = war.substring("file:".length());
		}

		try {
			if (isServiced(name)) {
				writer.println(smClient.getString("managerServlet.inService", displayPath));
			} else {
				addServiced(name);
				try {
					if (config != null) {
						if (!configBase.mkdirs() && !configBase.isDirectory()) {
							writer.println(smClient.getString(
									"managerServlet.mkdirFail", configBase));
							return;
						}
						File localConfig = new File(configBase, baseName + ".xml");
						if (localConfig.isFile() && !localConfig.delete()) {
							writer.println(smClient.getString(
									"managerServlet.deleteFail", localConfig));
							return;
						}
						copy(new File(config), localConfig);
					}
					if (war != null) {
						File localWar;
						if (war.endsWith(".war")) {
							localWar = new File(deployed, baseName + ".war");
						} else {
							localWar = new File(deployed, baseName);
						}
						if (localWar.exists() && !ExpandWar.delete(localWar)) {
							writer.println(smClient.getString(
									"managerServlet.deleteFail", localWar));
							return;
						}
						copy(new File(war), localWar);
					}
					// Perform new deployment
					check(name);
				} finally {
					removeServiced(name);
				}
			}
			writeDeployResult(writer, smClient, name, displayPath);
		} catch (Throwable t) {
			ExceptionUtils.handleThrowable(t);
			log("ManagerServlet.install[" + displayPath + "]", t);
			writer.println(smClient.getString("managerServlet.exception",
					t.toString()));
		}

	}

	private void writeDeployResult(PrintWriter writer, StringManager smClient,
	                               String name, String displayPath) {
		Context deployed = (Context) host.findChild(name);
		if (deployed != null && deployed.getConfigured() &&
				deployed.getState().isAvailable()) {
			writer.println(smClient.getString(
					"managerServlet.deployed", displayPath));
		} else if (deployed != null && !deployed.getState().isAvailable()) {
			writer.println(smClient.getString(
					"managerServlet.deployedButNotStarted", displayPath));
		} else {
			// Something failed
			writer.println(smClient.getString(
					"managerServlet.deployFailed", displayPath));
		}
	}

	/**
	 * Render a list of the currently active Contexts in our virtual host.
	 *
	 * @param writer Writer to render to
	 */
	protected void list(PrintWriter writer, StringManager smClient) {

		if (debug >= 1)
			log("list: Listing contexts for virtual host '" +
					host.getName() + "'");

		writer.println(smClient.getString("managerServlet.listed",
				host.getName()));
		Container[] contexts = host.findChildren();
		for (int i = 0; i < contexts.length; i++) {
			Context context = (Context) contexts[i];
			if (context != null) {
				String displayPath = context.getPath();
				if (displayPath.equals(""))
					displayPath = "/";
				if (context.getState().isAvailable()) {
					writer.println(smClient.getString("managerServlet.listitem",
							displayPath,
							"running",
							"" + context.getManager().findSessions().length,
							context.getDocBase()));
				} else {
					writer.println(smClient.getString("managerServlet.listitem",
							displayPath,
							"stopped",
							"0",
							context.getDocBase()));
				}
			}
		}
	}

	/**
	 * Reload the web application at the specified context path.
	 *
	 * @param writer Writer to render to
	 * @param cn     Name of the application to be restarted
	 */
	protected void reload(PrintWriter writer, ContextName cn,
	                      StringManager smClient) {

		if (debug >= 1)
			log("restart: Reloading web application '" + cn + "'");

		if (!validateContextName(cn, writer, smClient)) {
			return;
		}

		try {
			Context context = (Context) host.findChild(cn.getName());
			if (context == null) {
				writer.println(smClient.getString("managerServlet.noContext",
						RequestUtil.filter(cn.getDisplayName())));
				return;
			}
			// It isn't possible for the manager to reload itself
			if (context.getName().equals(this.context.getName())) {
				writer.println(smClient.getString("managerServlet.noSelf"));
				return;
			}
			context.reload();
			writer.println(smClient.getString("managerServlet.reloaded",
					cn.getDisplayName()));
		} catch (Throwable t) {
			ExceptionUtils.handleThrowable(t);
			log("ManagerServlet.reload[" + cn.getDisplayName() + "]", t);
			writer.println(smClient.getString("managerServlet.exception",
					t.toString()));
		}

	}

	/**
	 * Render a list of available global JNDI resources.
	 *
	 * @param type Fully qualified class name of the resource type of interest,
	 *             or <code>null</code> to list resources of all types
	 */
	protected void resources(PrintWriter writer, String type,
	                         StringManager smClient) {

		if (debug >= 1) {
			if (type != null) {
				log("resources:  Listing resources of type " + type);
			} else {
				log("resources:  Listing resources of all types");
			}
		}

		// Is the global JNDI resources context available?
		if (global == null) {
			writer.println(smClient.getString("managerServlet.noGlobal"));
			return;
		}

		// Enumerate the global JNDI resources of the requested type
		if (type != null) {
			writer.println(smClient.getString("managerServlet.resourcesType",
					type));
		} else {
			writer.println(smClient.getString("managerServlet.resourcesAll"));
		}

		Class<?> clazz = null;
		try {
			if (type != null) {
				clazz = Class.forName(type);
			}
		} catch (Throwable t) {
			ExceptionUtils.handleThrowable(t);
			log("ManagerServlet.resources[" + type + "]", t);
			writer.println(smClient.getString("managerServlet.exception",
					t.toString()));
			return;
		}

		printResources(writer, "", global, type, clazz, smClient);

	}

	/**
	 * List the resources of the given context.
	 */
	protected void printResources(PrintWriter writer, String prefix,
	                              javax.naming.Context namingContext,
	                              String type, Class<?> clazz,
	                              StringManager smClient) {

		try {
			NamingEnumeration<Binding> items = namingContext.listBindings("");
			while (items.hasMore()) {
				Binding item = items.next();
				if (item.getObject() instanceof javax.naming.Context) {
					printResources
							(writer, prefix + item.getName() + "/",
									(javax.naming.Context) item.getObject(), type, clazz,
									smClient);
				} else {
					if ((clazz != null) &&
							(!(clazz.isInstance(item.getObject())))) {
						continue;
					}
					writer.print(prefix + item.getName());
					writer.print(':');
					writer.print(item.getClassName());
					// Do we want a description if available?
					writer.println();
				}
			}
		} catch (Throwable t) {
			ExceptionUtils.handleThrowable(t);
			log("ManagerServlet.resources[" + type + "]", t);
			writer.println(smClient.getString("managerServlet.exception",
					t.toString()));
		}

	}

	/**
	 * Writes System OS and JVM properties.
	 *
	 * @param writer Writer to render to
	 */
	protected void serverinfo(PrintWriter writer, StringManager smClient) {
		if (debug >= 1)
			log("serverinfo");
		try {
			StringBuilder props = new StringBuilder();
			props.append("OK - Server info");
			props.append("\nTomcat Version: ");
			props.append(ServerInfo.getServerInfo());
			props.append("\nOS Name: ");
			props.append(System.getProperty("os.name"));
			props.append("\nOS Version: ");
			props.append(System.getProperty("os.version"));
			props.append("\nOS Architecture: ");
			props.append(System.getProperty("os.arch"));
			props.append("\nJVM Version: ");
			props.append(System.getProperty("java.runtime.version"));
			props.append("\nJVM Vendor: ");
			props.append(System.getProperty("java.vm.vendor"));
			writer.println(props.toString());
		} catch (Throwable t) {
			ExceptionUtils.handleThrowable(t);
			getServletContext().log("ManagerServlet.serverinfo", t);
			writer.println(smClient.getString("managerServlet.exception",
					t.toString()));
		}
	}

	/**
	 * Session information for the web application at the specified context path.
	 * Displays a profile of session thisAccessedTime listing number
	 * of sessions for each 10 minute interval up to 10 hours.
	 *
	 * @param writer Writer to render to
	 * @param cn     Name of the application to list session information for
	 * @param idle   Expire all sessions with idle time &gt; idle for this context
	 */
	protected void sessions(PrintWriter writer, ContextName cn, int idle,
	                        StringManager smClient) {

		if (debug >= 1) {
			log("sessions: Session information for web application '" + cn + "'");
			if (idle >= 0)
				log("sessions: Session expiration for " + idle + " minutes '" + cn + "'");
		}

		if (!validateContextName(cn, writer, smClient)) {
			return;
		}

		String displayPath = cn.getDisplayName();

		try {
			Context context = (Context) host.findChild(cn.getName());
			if (context == null) {
				writer.println(smClient.getString("managerServlet.noContext",
						RequestUtil.filter(displayPath)));
				return;
			}
			Manager manager = context.getManager();
			if (manager == null) {
				writer.println(smClient.getString("managerServlet.noManager",
						RequestUtil.filter(displayPath)));
				return;
			}
			int maxCount = 60;
			int histoInterval = 1;
			int maxInactiveInterval = context.getSessionTimeout();
			if (maxInactiveInterval > 0) {
				histoInterval = maxInactiveInterval / maxCount;
				if (histoInterval * maxCount < maxInactiveInterval)
					histoInterval++;
				if (0 == histoInterval)
					histoInterval = 1;
				maxCount = maxInactiveInterval / histoInterval;
				if (histoInterval * maxCount < maxInactiveInterval)
					maxCount++;
			}
			writer.println(smClient.getString("managerServlet.sessions",
					displayPath));
			writer.println(smClient.getString(
					"managerServlet.sessiondefaultmax",
					"" + maxInactiveInterval));
			Session[] sessions = manager.findSessions();
			int[] timeout = new int[maxCount + 1];
			int notimeout = 0;
			int expired = 0;
			long now = System.currentTimeMillis();
			for (int i = 0; i < sessions.length; i++) {
				int time;
				if (LAST_ACCESS_AT_START) {
					time = (int) ((now - sessions[i].getLastAccessedTimeInternal()) / 1000L);
				} else {
					time = (int) ((now - sessions[i].getThisAccessedTimeInternal()) / 1000L);
				}
				if (idle >= 0 && time >= idle * 60) {
					sessions[i].expire();
					expired++;
				}
				time = time / 60 / histoInterval;
				if (time < 0)
					notimeout++;
				else if (time >= maxCount)
					timeout[maxCount]++;
				else
					timeout[time]++;
			}
			if (timeout[0] > 0)
				writer.println(smClient.getString(
						"managerServlet.sessiontimeout",
						"<" + histoInterval, "" + timeout[0]));
			for (int i = 1; i < maxCount; i++) {
				if (timeout[i] > 0)
					writer.println(smClient.getString(
							"managerServlet.sessiontimeout",
							"" + (i) * histoInterval + " - <" + (i + 1) * histoInterval,
							"" + timeout[i]));
			}
			if (timeout[maxCount] > 0) {
				writer.println(smClient.getString(
						"managerServlet.sessiontimeout",
						">=" + maxCount * histoInterval,
						"" + timeout[maxCount]));
			}
			if (notimeout > 0)
				writer.println(smClient.getString(
						"managerServlet.sessiontimeout.unlimited",
						"" + notimeout));
			if (idle >= 0)
				writer.println(smClient.getString(
						"managerServlet.sessiontimeout.expired",
						">" + idle, "" + expired));
		} catch (Throwable t) {
			ExceptionUtils.handleThrowable(t);
			log("ManagerServlet.sessions[" + displayPath + "]", t);
			writer.println(smClient.getString("managerServlet.exception",
					t.toString()));
		}

	}

	/**
	 * Session information for the web application at the specified context path.
	 * Displays a profile of session thisAccessedTime listing number
	 * of sessions for each 10 minute interval up to 10 hours.
	 *
	 * @param writer Writer to render to
	 * @param cn     Name of the application to list session information for
	 * @deprecated Use {@link #sessions(PrintWriter, ContextName, int,
	 * StringManager)}
	 */
	@Deprecated
	protected void sessions(PrintWriter writer, ContextName cn,
	                        StringManager smClient) {
		sessions(writer, cn, -1, smClient);
	}

	/**
	 * Extract the expiration request parameter
	 *
	 * @param cn
	 * @param req
	 */
	protected void expireSessions(PrintWriter writer, ContextName cn,
	                              HttpServletRequest req, StringManager smClient) {
		int idle = -1;
		String idleParam = req.getParameter("idle");
		if (idleParam != null) {
			try {
				idle = Integer.parseInt(idleParam);
			} catch (NumberFormatException e) {
				log("Could not parse idle parameter to an int: " + idleParam);
			}
		}
		sessions(writer, cn, idle, smClient);
	}


	// -------------------------------------------------------- Support Methods

	/**
	 * Start the web application at the specified context path.
	 *
	 * @param writer Writer to render to
	 * @param cn     Name of the application to be started
	 */
	protected void start(PrintWriter writer, ContextName cn,
	                     StringManager smClient) {

		if (debug >= 1)
			log("start: Starting web application '" + cn + "'");

		if (!validateContextName(cn, writer, smClient)) {
			return;
		}

		String displayPath = cn.getDisplayName();

		try {
			Context context = (Context) host.findChild(cn.getName());
			if (context == null) {
				writer.println(smClient.getString("managerServlet.noContext",
						RequestUtil.filter(displayPath)));
				return;
			}
			context.start();
			if (context.getState().isAvailable())
				writer.println(smClient.getString("managerServlet.started",
						displayPath));
			else
				writer.println(smClient.getString("managerServlet.startFailed",
						displayPath));
		} catch (Throwable t) {
			ExceptionUtils.handleThrowable(t);
			getServletContext().log(sm.getString("managerServlet.startFailed",
					displayPath), t);
			writer.println(smClient.getString("managerServlet.startFailed",
					displayPath));
			writer.println(smClient.getString("managerServlet.exception",
					t.toString()));
		}

	}

	/**
	 * Stop the web application at the specified context path.
	 *
	 * @param writer Writer to render to
	 * @param cn     Name of the application to be stopped
	 */
	protected void stop(PrintWriter writer, ContextName cn,
	                    StringManager smClient) {

		if (debug >= 1)
			log("stop: Stopping web application '" + cn + "'");

		if (!validateContextName(cn, writer, smClient)) {
			return;
		}

		String displayPath = cn.getDisplayName();

		try {
			Context context = (Context) host.findChild(cn.getName());
			if (context == null) {
				writer.println(smClient.getString("managerServlet.noContext",
						RequestUtil.filter(displayPath)));
				return;
			}
			// It isn't possible for the manager to stop itself
			if (context.getName().equals(this.context.getName())) {
				writer.println(smClient.getString("managerServlet.noSelf"));
				return;
			}
			context.stop();
			writer.println(smClient.getString(
					"managerServlet.stopped", displayPath));
		} catch (Throwable t) {
			ExceptionUtils.handleThrowable(t);
			log("ManagerServlet.stop[" + displayPath + "]", t);
			writer.println(smClient.getString("managerServlet.exception",
					t.toString()));
		}

	}

	/**
	 * Undeploy the web application at the specified context path.
	 *
	 * @param writer Writer to render to
	 * @param cn     Name of the application to be removed
	 */
	protected void undeploy(PrintWriter writer, ContextName cn,
	                        StringManager smClient) {

		if (debug >= 1)
			log("undeploy: Undeploying web application at '" + cn + "'");

		if (!validateContextName(cn, writer, smClient)) {
			return;
		}

		String name = cn.getName();
		String baseName = cn.getBaseName();
		String displayPath = cn.getDisplayName();

		try {

			// Validate the Context of the specified application
			Context context = (Context) host.findChild(name);
			if (context == null) {
				writer.println(smClient.getString("managerServlet.noContext",
						RequestUtil.filter(displayPath)));
				return;
			}

			if (!isDeployed(name)) {
				writer.println(smClient.getString("managerServlet.notDeployed",
						RequestUtil.filter(displayPath)));
				return;
			}

			if (isServiced(name)) {
				writer.println(smClient.getString("managerServlet.inService", displayPath));
			} else {
				addServiced(name);
				try {
					// Try to stop the context first to be nicer
					context.stop();
				} catch (Throwable t) {
					ExceptionUtils.handleThrowable(t);
				}
				try {
					File war = new File(deployed, baseName + ".war");
					File dir = new File(deployed, baseName);
					File xml = new File(configBase, baseName + ".xml");
					if (war.exists() && !war.delete()) {
						writer.println(smClient.getString(
								"managerServlet.deleteFail", war));
						return;
					} else if (dir.exists() && !undeployDir(dir)) {
						writer.println(smClient.getString(
								"managerServlet.deleteFail", dir));
						return;
					} else if (xml.exists() && !xml.delete()) {
						writer.println(smClient.getString(
								"managerServlet.deleteFail", xml));
						return;
					}
					// Perform new deployment
					check(name);
				} finally {
					removeServiced(name);
				}
			}
			writer.println(smClient.getString("managerServlet.undeployed",
					displayPath));
		} catch (Throwable t) {
			ExceptionUtils.handleThrowable(t);
			log("ManagerServlet.undeploy[" + displayPath + "]", t);
			writer.println(smClient.getString("managerServlet.exception",
					t.toString()));
		}

	}

	/**
	 * Return a File object representing the "application root" directory
	 * for our associated Host.
	 *
	 * @deprecated Unused
	 */
	@Deprecated
	protected File getAppBase() {

		if (appBase != null) {
			return appBase;
		}

		File file = new File(host.getAppBase());
		if (!file.isAbsolute())
			file = new File(System.getProperty(Globals.CATALINA_BASE_PROP),
					host.getAppBase());
		try {
			appBase = file.getCanonicalFile();
		} catch (IOException e) {
			appBase = file;
		}
		return (appBase);

	}

	/**
	 * Invoke the isDeployed method on the deployer.
	 */
	protected boolean isDeployed(String name)
			throws Exception {
		String[] params = {name};
		String[] signature = {"java.lang.String"};
		Boolean result =
				(Boolean) mBeanServer.invoke(oname, "isDeployed", params, signature);
		return result.booleanValue();
	}

	/**
	 * Invoke the check method on the deployer.
	 */
	protected void check(String name)
			throws Exception {
		String[] params = {name};
		String[] signature = {"java.lang.String"};
		mBeanServer.invoke(oname, "check", params, signature);
	}

	/**
	 * Invoke the isServiced method on the deployer.
	 */
	protected boolean isServiced(String name)
			throws Exception {
		String[] params = {name};
		String[] signature = {"java.lang.String"};
		Boolean result =
				(Boolean) mBeanServer.invoke(oname, "isServiced", params, signature);
		return result.booleanValue();
	}

	/**
	 * Invoke the addServiced method on the deployer.
	 */
	protected void addServiced(String name)
			throws Exception {
		String[] params = {name};
		String[] signature = {"java.lang.String"};
		mBeanServer.invoke(oname, "addServiced", params, signature);
	}

	/**
	 * Invoke the removeServiced method on the deployer.
	 */
	protected void removeServiced(String name)
			throws Exception {
		String[] params = {name};
		String[] signature = {"java.lang.String"};
		mBeanServer.invoke(oname, "removeServiced", params, signature);
	}

	/**
	 * Delete the specified directory, including all of its contents and
	 * subdirectories recursively. The code assumes that the directory exists.
	 *
	 * @param dir File object representing the directory to be deleted.
	 */
	protected boolean undeployDir(File dir) {

		String files[] = dir.list();
		if (files == null) {
			files = new String[0];
		}
		for (int i = 0; i < files.length; i++) {
			File file = new File(dir, files[i]);
			if (file.isDirectory()) {
				if (!undeployDir(file)) {
					return false;
				}
			} else {
				if (!file.delete()) {
					return false;
				}
			}
		}
		return dir.delete();
	}

	/**
	 * Upload the WAR file included in this request, and store it at the
	 * specified file location.
	 *
	 * @param writer   Writer to render to
	 * @param request  The servlet request we are processing
	 * @param war      The file into which we should store the uploaded WAR
	 * @param smClient The StringManager used to construct i18n messages based
	 *                 on the Locale of the client
	 * @throws IOException if an I/O error occurs during processing
	 */
	protected void uploadWar(PrintWriter writer, HttpServletRequest request,
	                         File war, StringManager smClient) throws IOException {

		if (war.exists() && !war.delete()) {
			String msg = smClient.getString("managerServlet.deleteFail", war);
			throw new IOException(msg);
		}
		ServletInputStream istream = null;
		BufferedOutputStream ostream = null;
		try {
			istream = request.getInputStream();
			ostream =
					new BufferedOutputStream(new FileOutputStream(war), 1024);
			byte buffer[] = new byte[1024];
			while (true) {
				int n = istream.read(buffer);
				if (n < 0) {
					break;
				}
				ostream.write(buffer, 0, n);
			}
			ostream.flush();
			ostream.close();
			ostream = null;
			istream.close();
			istream = null;
		} catch (IOException e) {
			if (war.exists() && !war.delete()) {
				writer.println(
						smClient.getString("managerServlet.deleteFail", war));
			}
			throw e;
		} finally {
			if (ostream != null) {
				try {
					ostream.close();
				} catch (Throwable t) {
					ExceptionUtils.handleThrowable(t);
				}
				ostream = null;
			}
			if (istream != null) {
				try {
					istream.close();
				} catch (Throwable t) {
					ExceptionUtils.handleThrowable(t);
				}
				istream = null;
			}
		}

	}

	/**
	 * @deprecated Use {@link StringManager#getManager(String, Enumeration)}.
	 * This method will be removed in Tomcat 8.
	 */
	@Deprecated
	protected StringManager getStringManager(HttpServletRequest req) {
		Enumeration<Locale> requestedLocales = req.getLocales();
		while (requestedLocales.hasMoreElements()) {
			Locale locale = requestedLocales.nextElement();
			StringManager result = StringManager.getManager(Constants.Package,
					locale);
			if (result.getLocale().equals(locale)) {
				return result;
			}
		}
		// Return the default
		return sm;
	}


}
